package com.zx.framework.file.upload.controller;

import com.zx.framework.cache.ehcache.CacheUtil;
import com.zx.framework.cache.param.IParamCache;
import com.zx.framework.core.util.BeanUtil;
import com.zx.framework.core.util.FileUtil;
import com.zx.framework.core.util.RandomUtil;
import com.zx.framework.core.util.StringUtil;
import com.zx.framework.file.upload.util.FileMergUtil;
import com.zx.framework.file.upload.vo.FileCtx;
import com.zx.framework.file.upload.vo.FileCtxVo;
import com.zx.framework.web.controller.BaseController;
import org.apache.log4j.Logger;
import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.net.URLDecoder;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 文件上传接口控制类,用于http文件上传<br/>
 * 名词解释:<br/>
 * 1.文件上传:将文件上传至服务器并由服务器保存并返回结果的过程<br/>
 * 2.单文件上传:一次性独立上传一个完整文件的过程(未实现)<br/>
 * 3.多文件上传:一次性上传多个完整文件的过程(未实现)<br/>
 * 4.分片文件上传:将一个完整文件切割成多个文件分片并分开上传至服务器,由服务器在一个稍后的时间点合并,存储并返回上传结果的过程<br/>
 * 5.多分片上传:将一个完整文件切割成多个文件分片并在一次上传过程中上传多个分片的上传方式(未实现)<br/>
 */
@Controller
@RequestMapping("/file")
public class FileController extends BaseController {

    @Resource
    private IParamCache paramEhCache;

    private static CacheUtil cacheUtil = CacheUtil.getInstance();

    private static final Logger LOGGER = Logger.getLogger(FileController.class);

    /**
     * 初始化时间绑定
     *
     * @param binder web data binder
     * @throws Exception
     */
    @InitBinder
    public void dateBinder(WebDataBinder binder) throws Exception {
        DateFormat formatHHmmss = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        CustomDateEditor dateEditorHHmmss = new CustomDateEditor(formatHHmmss, true);
        binder.registerCustomEditor(Date.class, dateEditorHHmmss);
    }

    /**
     * 单文件完整上传方法
     *
     * @return
     */
    @RequestMapping("/upload")
    @ResponseBody
    public Object upload(@RequestParam(value = "token", required = false) String token,
                         @RequestParam(value = "sbbh", required = false) String sbbh,
                         @RequestParam(value = "file", required = false) MultipartFile multipartFile,
                         @RequestParam(value = "fileName", required = false) String fileName,
                         @RequestParam(value = "bucket", required = false) String bucket,
                         @RequestParam(value = "fileSize", required = false) Long fileSize,
                         @RequestParam(value = "fileCrc32", required = false) Long fileCrc32) {
        if (multipartFile == null) {
            throw new UploadException("上传文件不能为空！");// 上传文件不存在
        }
        // 处理时机问题再研究
        String baseFilePath = paramEhCache.get("FILE.PATH").getCode();
        return null;
    }

    /**
     * 分片上传,创建块,用于初始创建文件上传块<br/>
     * 此接口要求将要上传的文件基本数据同时上传第一个分片,<br/>
     * 服务端会保存并计算第一个分片的crc32并返回,<br/>
     * 同时返回此文件上传ctxKey<br/>
     *
     * @param token         token
     * @param sbbh          设备编号
     * @param deadline      上传请求授权的截止时间
     * @param fileName      上传文件的文件名
     * @param insertOnly    仅插入,当此值不为0时,接口只允许插入数据
     * @param bucket        上传请求存储资源空间
     * @param fileSize      上传文件大小
     * @param fileCrc32     上传文件的crc32校验值
     * @param chunkSize     上传分片大小
     * @param file 上传文件
     * @return fileCtx
     */
    @RequestMapping("/mkblk")
    @ResponseBody
    public Object mkblk(@RequestParam(value = "token", required = false) String token,
                        @RequestParam(value = "sbbh", required = false) String sbbh,
                        @RequestParam(value = "fileName", required = true) String fileName,
                        @RequestParam(value = "deadline", required = false) Date deadline,
                        @RequestParam(value = "insertOnly" ,defaultValue = "1") String insertOnly,
                        @RequestParam(value = "bucket", required = true) String bucket,
                        @RequestParam(value = "chunkSize", required = false) Long chunkSize,
                        @RequestParam(value = "fileSize", required = false) Long fileSize,
                        @RequestParam(value = "fileCrc32", required = false) Long fileCrc32,
                        @RequestParam(value = "file", required = true) MultipartFile file) {
        try{
            if (file == null) {
                throw new UploadException("上传文件不能为空！");// 上传文件不存在
            }
            if (StringUtil.isEmpty(bucket)){
                throw new UploadException("bucket can NOT EMPTY！");
            }
            String baseFilePath = paramEhCache.get("file.upload.path").getCode();
            File savePath = new File(baseFilePath+File.separator+bucket);
            if (!savePath.exists()){
                boolean mkSavePath = savePath.mkdirs();
                LOGGER.debug("make file bucket path:"+mkSavePath);
            }

            LOGGER.debug("file name before decode:"+fileName);
            fileName = URLDecoder.decode(fileName,"utf-8");
            LOGGER.debug("file name after decode:"+fileName);

            File saveFile = new File(savePath.getAbsolutePath()+File.separator+fileName);
            if (saveFile.exists() && !"0".equals(insertOnly)){
                throw new UploadException("file is already exists!");
            }
            if (saveFile.exists() && "0".equals(insertOnly)){
                boolean deleteSaveFile = saveFile.delete();
                LOGGER.debug("delete save file:"+deleteSaveFile);
            }
            try {
                FileUtil.writeStringToFile(saveFile,"");
            } catch (IOException e) {
                LOGGER.error("init file space error!",e);
                throw new UploadException("init file space error:"+e.getMessage());
            }
            String ctx = RandomUtil.getUUID();
            File tmpPath = new File(savePath.getAbsolutePath()+File.separator+ctx);
            boolean mkCtxPath = tmpPath.mkdirs();
            LOGGER.debug("make file ctx tmp path:"+mkCtxPath);

            File firstChunk = new File(tmpPath.getAbsolutePath()+File.separator+ctx);
            try {
                FileUtil.copyInputStreamToFile(file.getInputStream(),firstChunk);
            } catch (IOException e) {
                LOGGER.error("init file space error!",e);
                throw new UploadException("init file space error:"+e.getMessage());
            }

            long firstChunkCrc32;
            try {
                firstChunkCrc32 = FileUtil.checksumCRC32(firstChunk);
            } catch (IOException e) {
                LOGGER.error("check first chunk crc32 error!", e);
                throw new UploadException("check first chunk crc32 error:"+e.getMessage());
            }

            FileCtx fileCtx = new FileCtx(fileName,saveFile,tmpPath,fileCrc32,fileSize,chunkSize,deadline,bucket,ctx);
            fileCtx.putChunk(ctx,firstChunk);

            FileCtxVo fileCtxVo = new FileCtxVo();
            BeanUtil.copyProperties(fileCtx,fileCtxVo);
            fileCtxVo.setChunkCrc32(firstChunkCrc32);
            fileCtxVo.setChunkCtx(ctx);

            cacheUtil.put(ctx,fileCtx);
            return fileCtxVo;
        }catch (Exception e){
            LOGGER.error("file mkblk unknow exception:"+e.getMessage(),e);
            throw new UploadException("file mkblk unknow exception:"+e.getMessage(),e);
        }
    }

    /**
     * 分片上传,上传文件分片
     * @param token         token
     * @param sbbh          设备编号
     * @param ctxKey        上传分片时的ctxKey
     * @param chunkOffset   分片在整个文件中的起始位置
     * @param chunkSize     分片大小
     * @param file          分片文件
     * @return  fileCtx
     */
    @RequestMapping("/bput")
    @ResponseBody
    public Object bput(@RequestParam(value = "token", required = false) String token,
                       @RequestParam(value = "sbbh", required = false) String sbbh,
                       @RequestParam(value = "ctxKey", required = true) String ctxKey,
                       @RequestParam(value = "chunkOffset", required = false) Long chunkOffset,
                       @RequestParam(value = "chunkSize", required = false) Long chunkSize,
                       @RequestParam(value = "file", required = true) MultipartFile file) {
        try {
            if (file == null) {
                throw new UploadException("上传文件不能为空！");// 上传文件不存在
            }
            FileCtx fileCtx = cacheUtil.get(ctxKey, FileCtx.class);
            if (null == fileCtx) {
                throw new UploadException("ctx key err: can not find file ctx!");
            }
            String baseFilePath = paramEhCache.get("file.upload.path").getCode();

            String ctx = RandomUtil.getUUID();
            File tmpPath = new File(baseFilePath + File.separator + fileCtx.getBucket() + File.separator + fileCtx.getCtxKey());
            LOGGER.debug("make file ctx tmp path:" + tmpPath.getAbsolutePath());

            File chunk = new File(tmpPath.getAbsolutePath() + File.separator + ctx);
            try {
                FileUtil.copyInputStreamToFile(file.getInputStream(), chunk);
            } catch (IOException e) {
                LOGGER.error("init file space error!", e);
                throw new UploadException("init file space error:" + e.getMessage());
            }

            long chunkCrc32;
            try {
                chunkCrc32 = FileUtil.checksumCRC32(chunk);
            } catch (IOException e) {
                LOGGER.error("check first chunk crc32 error!", e);
                throw new UploadException("check first chunk crc32 error:" + e.getMessage());
            }

            fileCtx.putChunk(ctx, chunk);

            FileCtxVo fileCtxVo = new FileCtxVo();
            BeanUtil.copyProperties(fileCtx, fileCtxVo);
            fileCtxVo.setChunkCrc32(chunkCrc32);
            fileCtxVo.setChunkCtx(ctx);

            cacheUtil.put(ctx, fileCtx);
            return fileCtxVo;
        }catch (Exception e){
            LOGGER.error("file bput unknow exception:"+e.getMessage(),e);
            throw new UploadException("file bput unknow exception:"+e.getMessage(),e);
        }
    }

    /**
     * 分片上传,创建文件
     * @param token     token
     * @param sbbh      sbbh
     * @param ctxKey    上传分片时的ctxKey
     * @param ctxArray  分片序列串ctxArray
     * @return fileCtx
     */
    @RequestMapping("/mkfile")
    @ResponseBody
    public Object mkfile(@RequestParam(value = "token", required = false) String token,
                         @RequestParam(value = "sbbh", required = false) String sbbh,
                         @RequestParam(value = "ctxKey", required = false) String ctxKey,
                         @RequestParam(value = "ctxArray", required = false) String ctxArray) {
        try {
            FileCtx fileCtx = cacheUtil.get(ctxKey,FileCtx.class);
            if (null == fileCtx){
                throw new UploadException("ctx key err: can not find file ctx!");
            }
            FileMergUtil.mergFile(fileCtx, ctxArray);

            FileCtxVo fileCtxVo = new FileCtxVo();
            BeanUtil.copyProperties(fileCtx,fileCtxVo);
            fileCtxVo.setChunkCrc32(0L);
            fileCtxVo.setChunkCtx("");
            fileCtxVo.setCtxArray(ctxArray);

            try {
                FileUtil.deleteDirectory(fileCtx.getTmpPath());
            } catch (IOException e) {
                LOGGER.warn("delete tmp path error!", e);
            }
            return fileCtxVo;
        }catch (Exception e){
            LOGGER.error("file mkfile unknow exception:"+e.getMessage(),e);
            throw new UploadException("file mkfile unknow exception:"+e.getMessage(),e);
        }
    }

    /**
     * 移除文件
     * @param token     token
     * @param sbbh      设备编号
     * @param bucket    资源存储空间
     * @param fileName  文件名
     * @return  true/false
     */
    @RequestMapping("/rm")
    @ResponseBody
    private Object rm(@RequestParam(value = "token", required = false) String token,
                      @RequestParam(value = "sbbh", required = false) String sbbh,
                      @RequestParam(value = "bucket", required = true) String bucket,
                      @RequestParam(value = "fileName", required = false) String fileName){
        if (StringUtil.isEmpty(bucket)){
            throw new UploadException("bucket can NOT EMPTY！");
        }

        String baseFilePath = paramEhCache.get("file.upload.path").getCode();
        if (StringUtil.isNotEmpty(fileName)){
            return (new File(baseFilePath+File.separator+bucket+File.separator+fileName)).delete();
        } else {
            try {
                FileUtil.deleteDirectory(new File(baseFilePath+File.separator+bucket));
                return true;
            } catch (IOException e) {
                return false;
            }
        }
    }
}
